﻿using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using System.Linq.Expressions;

namespace Microsoft.EntityFrameworkCore
{
    /// <summary>
    /// Converts nullable enum values to and from their underlying nullable numeric representation.
    /// </summary>
    /// <typeparam name="TEnumNullable">enum type</typeparam>
    /// <typeparam name="TNumberNullable">numberic type</typeparam>
    public class NullableEnumToNumberConverter<TEnumNullable, TNumberNullable> : ValueConverter<TEnumNullable, TNumberNullable>
    {
        private static readonly ConverterMappingHints _defaultHints = CreateDefaultHints();
        public NullableEnumToNumberConverter([CanBeNull] ConverterMappingHints mappingHints = null)
            : base(
                ToNullableNumber(),
                ToNullableEnum(),
                _defaultHints?.With(mappingHints) ?? mappingHints)
        {
        }

        private static ConverterMappingHints CreateDefaultHints()
        {
            var underlyingModelType = typeof(TEnumNullable).UnwrapEnumType();

            return (underlyingModelType == typeof(long) || underlyingModelType == typeof(ulong))
                && typeof(TNumberNullable).UnwrapNullableType() == typeof(decimal)
                    ? new ConverterMappingHints(precision: 20, scale: 0)
                    : default;
        }

        private static Expression<Func<TEnumNullable, TNumberNullable>> ToNullableNumber()
        {
            var modelType = typeof(TEnumNullable);
            var underlyingModelType = modelType.UnwrapNullableType();
            var providerType = typeof(TNumberNullable);
            var underlyingProviderType = providerType.UnwrapNullableType();

            if (!modelType.IsNullableValueType() || !underlyingModelType.IsEnum)
            {
                throw new InvalidOperationException(
                    CoreStrings.ConverterBadType(
                        typeof(NullableEnumToNumberConverter<TEnumNullable, TNumberNullable>).ShortDisplayName(),
                        typeof(TEnumNullable).ShortDisplayName(),
                        "nullable enum types"));
            }

            if (!providerType.IsNullableValueType())
            {
                throw new InvalidOperationException(
                   CoreStrings.ConverterBadType(
                       typeof(NullableEnumToNumberConverter<TEnumNullable, TNumberNullable>).ShortDisplayName(),
                       typeof(TNumberNullable).ShortDisplayName(),
                       "nullable numberic types"));
            }

            CheckTypeSupported(
                underlyingProviderType,
                typeof(NullableEnumToNumberConverter<TEnumNullable, TNumberNullable>),
                typeof(int), typeof(long), typeof(short), typeof(byte),
                typeof(uint), typeof(ulong), typeof(ushort), typeof(sbyte),
                typeof(double), typeof(float), typeof(decimal));

            var param = Expression.Parameter(modelType, "value");
            var hasValueExpression = Expression.Property(param, modelType.GetProperty("HasValue"));
            var changeExpression = underlyingProviderType == typeof(decimal) ? Expression.Convert(param, typeof(long)) : (Expression)param;
            var convertExpression = Expression.Convert(changeExpression, providerType);
            var body = Expression.Condition(hasValueExpression, convertExpression, Expression.New(providerType));

            return Expression.Lambda<Func<TEnumNullable, TNumberNullable>>(body, param);

        }

        private static Expression<Func<TNumberNullable, TEnumNullable>> ToNullableEnum()
        {
            var modelType = typeof(TEnumNullable);
            var providerType = typeof(TNumberNullable);
            var underlyingProviderType = providerType.UnwrapNullableType();

            var param = Expression.Parameter(providerType, "value");
            var hasValueExpression = Expression.Property(param, providerType.GetProperty("HasValue"));
            var changeExpression = underlyingProviderType == typeof(decimal) ? Expression.Convert(param, typeof(long)) : (Expression)param;
            var convertExpression = Expression.Convert(changeExpression, modelType);
            var expresion = Expression.Condition(hasValueExpression, convertExpression, Expression.New(modelType));

            return Expression.Lambda<Func<TNumberNullable, TEnumNullable>>(expresion, param);
        }
    }
}
